package nz.ac.massey.groupproject.ecs2;

import java.util.List;
import java.util.ArrayList;

import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;


public class ECSSystemUtil {
	// TODO pass globalData to Systems nicely instead of passing it everywhere (access via ECSWorld? BYO pointer?)

	// ======================================== highlevel concept ========================================
	public static void smartTick(List<ECSSystem> pipeline, ECSWorld world) {
		// FIXME very slow
		var oldEnts = new ArrayList<ECSEntity>(world.entities);

		for (var sys : pipeline)
			smartTick(sys, oldEnts, world);
	}
	public static void smartTick(ECSSystem sys, List<ECSEntity> allEnts, ECSWorld world) {
		tryInvoke(sys, "globalTick", world);
		invokeOnMatchingEntities(sys, "tick", allEnts, world);
	}







	// ======================================== smart invocation ========================================
	public static void tryInvoke(ECSSystem sys, String methName, ECSWorld world) {
		for (var meth: sys.getClass().getDeclaredMethods()) {

			if (! meth.getName().equals(methName))
				continue;
			tryInvoke(sys, meth, world);
		}
	}

	public static void tryInvoke(ECSSystem sys, Method meth, ECSWorld world) {
		Object globalData = world.globalData;
		int argc = meth.getParameterCount();
		var paramTs = meth.getGenericParameterTypes();

		if (0 == argc)
			callMethodIgnoringExceptions(sys, meth);
		else if (1 == argc && ECSWorld.class == paramTs[0])
			callMethodIgnoringExceptions(sys, meth, world);
		else if (1 == argc && globalData.getClass() == paramTs[0])
			callMethodIgnoringExceptions(sys, meth, globalData);
		// TODO else sys.meth(world, globalData)
		//      else sys.meth(globalData, world)
	}



	public static void invokeOnMatchingEntities(ECSSystem sys, String methName, List<ECSEntity> ents, ECSWorld world) {
		for (var meth : sys.getClass().getDeclaredMethods())
			if (meth.getName().equals(methName))
				invokeOnMatchingEntities(sys, meth, ents, world);
	}

	public static void invokeOnMatchingEntities(ECSSystem sys, Method meth, List<ECSEntity> ents, ECSWorld world) {
		Object globalData = world.globalData;

		// sanity check   (early out before looping over thousands of entities)
		var paramTs = meth.getGenericParameterTypes();
		int argc   = meth.getParameterCount();
		var args   = new Object[argc];
		var isComp = new boolean[argc];

		for (int ai = 0; ai < argc; ai++) {
			var paramT = (Class) paramTs[ai];
			if      (ECSWorld  .class      == paramT)           { isComp[ai] = false;  args[ai] = world;      }
			else if (globalData.getClass() == paramT)           { isComp[ai] = false;  args[ai] = globalData; }
			else if (ECSEntity .class      == paramT)             isComp[ai] = false;
			else if (ECSComponent.class.isAssignableFrom(paramT)) isComp[ai] = true;
			else {
				// not a valid callback           TODO log a warning
				return;
			}
		}

		// loop and match
		for (var ent : ents) {
			boolean match = true;
			for (int ai = 0; ai < argc; ai++) {
				var paramT = (Class) paramTs[ai];

				if (ECSEntity.class == paramT)
					args[ai] = ent;

				else if (isComp[ai]) {
					args[ai] = ent.getComponent(paramT);

					if (null == args[ai]) { match = false; break; }
				}
				// else do nothing
			}

			if (match) callMethodIgnoringExceptions(sys, meth, args);
		}
	}





	// ======================================== reflection utils ========================================
	// TODO move away

	// method should not throw checked exceptions
	public static Object callMethodIgnoringExceptions(Object receiver, Method meth, Object... args) {
		try {
			return meth.invoke(receiver, args);

		} catch (IllegalAccessException e) {
			throw new RuntimeException("Attempting to invoke inaccessible method (private, etc)", e);

		} catch (InvocationTargetException ee) {
			Throwable e = ee.getCause();
			if (e instanceof Error)             throw (Error) e;
			if (e instanceof RuntimeException)  throw (RuntimeException) e;
			throw new AssertionError("Methods that throw checked exceptions should not be invoked through reflection", e);
		}
	}
}
